using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.Runtime.Serialization;

namespace Rogue
{
    [Serializable]
    public class GameCamera
    {
        #region Public Member Variables
        public enum Zoom { Normal, Wide }
        public Zoom CurrentZoom = Zoom.Normal;

        public enum MoveState { None, GlidingToEntity, GlidingToPos, MouseControl  }
        public MoveState CurrentMoveState = MoveState.None;

        private Vector2 m_vMoveDest;
        private Vector2 m_vMoveOrigin;
        private double m_GlideTime;
 
        /// <summary>
        /// Returns the offset (in cell units) that the camera is using to center itself on the window
        /// </summary>
        public Point Offset
        {
            get
            {
                return new Point(
                    (int)Math.Floor((float)TerrainWindow.Instance.Width / 2.0f),
                    (int)Math.Floor((float)TerrainWindow.Instance.Height / 2.0f)
                    );
            }
        }

        /// <summary>
        /// Returns coordinates (in cell units) of the terrain window that will be updated
        /// </summary>
        public Rectangle RenderableArea
        {
            get
            {
                if (TerrainWindow.Instance == null)
                    return new Rectangle();

                Point pOffset = Offset; 
                return new Rectangle(
                    m_pPosition.X - pOffset.X,
                    m_pPosition.Y - pOffset.Y,
                    TerrainWindow.Instance.Width - pOffset.X,
                    TerrainWindow.Instance.Height - pOffset.Y
                    );
            }
        }

        int clamp(int a, int min, int max)
        {
            return Math.Max(min, Math.Min(max, a));
        }

        // returns coords in window space (ie cell space)!
        public Rectangle GetTerrainWindowRenderableArea(GameMap a_Map, bool a_unobscuredOnly)
        {
            Point pTopLeft = new Point(0, 0);
            Point pBottomRight = new Point(TerrainWindow.Instance.Width, TerrainWindow.Instance.Height);

            if (a_unobscuredOnly)
            {
                int margin = 3;

                pTopLeft.X += margin;
                pTopLeft.Y += margin;
                pBottomRight.X -= margin;
                pBottomRight.Y -= margin;

                int consoleTopInCells = ConsoleWindow.Instance.Position.Y / TerrainWindow.Instance.CellScreenSize.Y;
                pBottomRight.Y = Math.Min(pBottomRight.Y, consoleTopInCells) - margin;

                if (DetailWindow.HeroWindow.IsActive)
                {
                    int heroRightInCells = DetailWindow.HeroWindow.WindowBounds.Right / TerrainWindow.Instance.CellScreenSize.X;
                    pTopLeft.X = Math.Max(pTopLeft.X, heroRightInCells) + margin;
                }

                if (DetailWindow.EnemyWindow.IsActive)
                {
                    int enemyLeftInCells = DetailWindow.EnemyWindow.WindowBounds.Left / TerrainWindow.Instance.CellScreenSize.X;
                    pBottomRight.X = Math.Min(pBottomRight.X, enemyLeftInCells) - margin;                        
                }

                if (TutWindow.Instance.IsActive)
                {
                    int tutBottomInCells = TutWindow.Instance.WindowBounds.Bottom / TerrainWindow.Instance.CellScreenSize.Y;
                    int tutTopInCells = TutWindow.Instance.WindowBounds.Top / TerrainWindow.Instance.CellScreenSize.Y;

                    // is the tutorial window closer to the top or bottom of the screen?
                    
                    if (tutTopInCells < TerrainWindow.Instance.Height - tutBottomInCells)   // closer to top
                    {
                        pTopLeft.Y = Math.Max(pTopLeft.Y, tutBottomInCells) + margin;
                    }
                    else
                    {
                        pBottomRight.Y = Math.Min(pBottomRight.Y, tutTopInCells) - margin;
                    }
                }
            }

            return new Rectangle(pTopLeft.X, pTopLeft.Y, pBottomRight.X - pTopLeft.X, pBottomRight.Y - pTopLeft.Y);
        }

        public Rectangle GetMapRenderableArea(GameMap a_Map, bool a_UnobscuredOnly)
        {
            Rectangle r = GetTerrainWindowRenderableArea(a_Map, a_UnobscuredOnly);
            r.Location = CellToMapSpace(new Point(r.Left, r.Top));

            return r;

        }
        /// <summary>
        /// Returns the current map coordinates that the mouse is over
        /// </summary>
        public Point MapMouseHoverCell
        {
            get
            {
                Point pWindowMouseHoverCell = TerrainWindow.Instance.MouseHoverCell;
                return new Point(
                    m_pPosition.X - Offset.X + pWindowMouseHoverCell.X,
                    m_pPosition.Y - Offset.Y + pWindowMouseHoverCell.Y
                    );
            }
        }
        #endregion

        #region Private Member Variables
        private Point m_pPosition;
        private Vector2 m_vPosition;
        private Entity m_TrackTarget;
        private Point m_LastTrackTargetPos;
        private Entity m_GlideTarget;

        private const float nBaseSpeed = 35.0f;
        private const float nSpeedMultiplier = 8.0f;
        private const float m_nGlideSpeedMultiplier = 1.5f;

        // Previous input states
        private KeyboardState m_LastKeyState;
        private MouseState m_LastMouseState;
        private GamePadState m_LastGamepadState;
        #endregion

        #region Constructor
        /// <summary>
        /// Constructor
        /// </summary>
        public GameCamera(int a_X, int a_Y)
        {
            m_pPosition = new Point(a_X, a_Y);
            m_vPosition = new Vector2(a_X, a_Y);
            m_LastKeyState = Keyboard.GetState();
            m_LastMouseState = Mouse.GetState();
            m_LastGamepadState = GamePad.GetState(PlayerIndex.One);
            m_vMoveDest = new Vector2();
            m_vMoveOrigin = new Vector2();
        }
        #endregion
        
        #region Update Loop
        public void Update(GameTime a_GameTime)
        {
            float dt = (float)a_GameTime.ElapsedGameTime.TotalSeconds;
            
            // Input states
            GamePadState gamepad = GamePad.GetState(PlayerIndex.One);
            KeyboardState kb = Keyboard.GetState();
            MouseState mouse = Mouse.GetState();
            
            Rectangle rWindowBounds = TerrainWindow.Instance.WindowBounds;
            Point pLocalMousePos = new Point(mouse.X - rWindowBounds.Left, mouse.Y - rWindowBounds.Top);

            if (IsTracking() && CurrentMoveState == MoveState.None)
            {
                Point pos = GetPosition();
                Rectangle bounds = RogueGame.Instance.Map.Camera.GetMapRenderableArea(RogueGame.Instance.Map, true);
                
                if (!bounds.Contains(m_TrackTarget.Position))
                    GlideTo(m_TrackTarget);
            }

            // he moved
            if (m_TrackTarget != null)
            {
                if (m_TrackTarget.Position != m_LastTrackTargetPos && mouse.MiddleButton != ButtonState.Pressed && CurrentMoveState == MoveState.MouseControl)
                {
                    GlideTo(m_TrackTarget);
                }

                m_LastTrackTargetPos = m_TrackTarget.Position;
            }

            

            if (CurrentMoveState == MoveState.None || CurrentMoveState == MoveState.MouseControl)
            {
                #region Check Zoom Button
                if (mouse.ScrollWheelValue < m_LastMouseState.ScrollWheelValue)
                    SetZoomLevel(Zoom.Wide);
                if (mouse.ScrollWheelValue > m_LastMouseState.ScrollWheelValue)
                    SetZoomLevel(Zoom.Normal);

                if (gamepad.IsButtonDown(Buttons.LeftStick) && !m_LastGamepadState.IsButtonDown(Buttons.LeftStick))
                    ToggleZoomLevel();

                if (!ConsoleWindow.Instance.IsActive)
                {
                    if (kb.IsKeyDown(Keys.Z) && !m_LastKeyState.IsKeyDown(Keys.Z))
                        ToggleZoomLevel();
                }
                #endregion

                #region Update Position


                #region GamePad Control
                float nGamePadMultiplier = 1.0f + (nSpeedMultiplier * gamepad.Triggers.Left);

                float nGamePadTransformX = (gamepad.ThumbSticks.Left.X * nBaseSpeed) * dt;
                nGamePadTransformX = nGamePadTransformX * nGamePadMultiplier;
                float nGamePadTransformY = -(gamepad.ThumbSticks.Left.Y * nBaseSpeed) * dt;
                nGamePadTransformY = nGamePadTransformY * nGamePadMultiplier;
                #endregion

                #region Keyboard Control
                float nKeyboardTransformX = 0;
                float nKeyboardTransformY = 0;
                float nShiftKeyMult = 1.0f;

                if (!ConsoleWindow.Instance.IsActive)
                {
                    if (kb.IsKeyDown(Keys.LeftShift) || kb.IsKeyDown(Keys.RightShift))
                        nShiftKeyMult = nSpeedMultiplier;

                    bool any = false;

                    if (kb.IsKeyDown(Keys.Left))
                    {
                        any = true;
                        nKeyboardTransformX = -(nBaseSpeed * nShiftKeyMult) * dt;
                    }
                    if (kb.IsKeyDown(Keys.Right))
                    {
                        any = true;
                        nKeyboardTransformX = (nBaseSpeed * nShiftKeyMult) * dt;
                    }
                    if (kb.IsKeyDown(Keys.Up))
                    {
                        any = true;
                        nKeyboardTransformY = -(nBaseSpeed * nShiftKeyMult) * dt;
                    }
                    if (kb.IsKeyDown(Keys.Down))
                    {
                        any = true;
                        nKeyboardTransformY = (nBaseSpeed * nShiftKeyMult) * dt;
                    }

                    if (any)
                    {
                        CurrentMoveState = MoveState.MouseControl;
                    }
                }
                #endregion

                #region Mouse Control
                float nMouseTransformX = 0;
                float nMouseTransformY = 0;

                if (mouse.MiddleButton == ButtonState.Pressed)
                {
                    CurrentMoveState = MoveState.MouseControl;
                    float speed = 0.2f;
                    if (CurrentZoom == Zoom.Wide)
                        speed = 0.1f;

                    nMouseTransformX = speed * (m_LastMouseState.X - mouse.X);
                    nMouseTransformY = speed * (m_LastMouseState.Y - mouse.Y);
                }
                #endregion

                // Update the position
                m_vPosition.X += nGamePadTransformX + nKeyboardTransformX + nMouseTransformX;
                m_vPosition.Y += nGamePadTransformY + nKeyboardTransformY + nMouseTransformY;
                m_pPosition = new Point((int)Math.Floor(m_vPosition.X), (int)Math.Floor(m_vPosition.Y));

                #endregion
            }
            else if (CurrentMoveState == MoveState.GlidingToEntity)
            {
                // put the target in the middle of our renderable area
                Rectangle bounds = RogueGame.Instance.Map.Camera.GetMapRenderableArea(RogueGame.Instance.Map, true);
                Point boundsMiddle = new Point(bounds.Location.X + bounds.Width / 2, bounds.Location.Y + bounds.Height / 2);

                Vector2 TargetPos = new Vector2(m_vPosition.X + m_GlideTarget.Position.X - boundsMiddle.X, m_vPosition.Y + m_GlideTarget.Position.Y - boundsMiddle.Y);
                float currentDist = Vector2.Distance(m_vPosition, TargetPos);
                if (currentDist < 0.1f)
                {
                    SetPosition(TargetPos);
                    CurrentMoveState = MoveState.None;
                }
                else
                {
                    SetPosition(Vector2.SmoothStep(m_vMoveOrigin, TargetPos, (float)(m_GlideTime * m_nGlideSpeedMultiplier)));
                    m_GlideTime += a_GameTime.ElapsedRealTime.TotalSeconds;
                }
            }

            else if (CurrentMoveState == MoveState.GlidingToPos)
            {
                float currentDist = Vector2.Distance(m_vPosition, m_vMoveDest);
                if (currentDist < 0.1f)
                {
                    SetPosition(m_vMoveDest);
                    CurrentMoveState = MoveState.None;
                }
                else
                {
                    SetPosition(Vector2.SmoothStep(m_vMoveOrigin, m_vMoveDest, (float)(m_GlideTime * m_nGlideSpeedMultiplier)));
                    m_GlideTime += a_GameTime.ElapsedRealTime.TotalSeconds;
                }
            }

            // Get the cell coordinates for the mouse
            Point pMouseLocalHoverCell = TerrainWindow.Instance.MouseHoverCell;
            Point pMouseMapHoverCell = CellToMapSpace(pMouseLocalHoverCell);
            Point pPositionOnScreen = CellToWindowSpace(m_pPosition);
            // Draw the outline
            // TerrainWindow.Instance.DrawCellOutline(pPositionOnScreen, new Color(255, 255, 255, 64));
            // TerrainWindow.Instance.DrawCellOutline(pMouseLocalHoverCell, Color.Yellow);

            // Store the control states
            m_LastGamepadState = gamepad;
            m_LastKeyState = kb;
            m_LastMouseState = mouse;

        }
        #endregion

        #region Public Position Functions
        public void SetPosition(Point a_pNewPos)
        {
            m_pPosition = a_pNewPos;
            m_vPosition = new Vector2(a_pNewPos.X + 0.5f, a_pNewPos.Y + 0.5f);
        }

        
        public void SetPosition(Vector2 a_vNewPos)
        {
            m_vPosition = a_vNewPos;
            m_pPosition = new Point((int)Math.Floor(a_vNewPos.X), (int)Math.Floor(a_vNewPos.Y));
        }

        public Point GetPosition()
        {
            return m_pPosition;
        }

        public Vector2 GetVectorPosition()
        {
            return m_vPosition;
        }

        public void SetTrackTarget(Entity a_Target)
        {
            m_TrackTarget = a_Target;
        }

        public Entity GetTrackTarget()
        {
            return m_TrackTarget;
        }
        public void ClearTrackTarget()
        {
            m_TrackTarget = null;
        }

        public bool IsTracking()
        {
            if (m_TrackTarget != null)
                return true;
            else
                return false;
        }

        public void GlideTo(Vector2 a_vDest)
        {
            if (CurrentMoveState != MoveState.None)
                return;

            m_vMoveDest = a_vDest;
            m_vMoveOrigin = m_vPosition;
            m_GlideTime = 0;
            CurrentMoveState = MoveState.GlidingToPos;
        }

        public void GlideTo(int a_nMapCellX, int a_nMapCellY)
        {
            if (CurrentMoveState != MoveState.None)
                return;

            m_vMoveDest = new Vector2(a_nMapCellX + 0.5f, a_nMapCellY + 0.5f);
            m_vMoveOrigin = m_vPosition;
            m_GlideTime = 0;
            CurrentMoveState = MoveState.GlidingToPos;
        }

        public void GlideTo(Point a_pDest)
        {
            GlideTo(a_pDest.X, a_pDest.Y);
        }

        public void GlideTo(Entity a_Target)
        {
            m_GlideTarget = a_Target;
            m_vMoveOrigin = m_vPosition;
            m_GlideTime = 0;
            CurrentMoveState = MoveState.GlidingToEntity;
        }

        #endregion

        #region Public Transformation Functions
        /// <summary>
        /// Returns the cell coordinates of the terrain window if you provide the map coordinates.
        /// </summary>
        public Point CellToWindowSpace(Point a_Point)
        {
            return new Point(
                -m_pPosition.X + Offset.X + a_Point.X,
                -m_pPosition.Y + Offset.Y + a_Point.Y
                );            
        }

        /// <summary>
        /// Returns a set of map coordinates from terrain window space
        /// if you provide the TerrainWindow coordinates. 
        /// </summary>
        public Point CellToMapSpace(Point a_Point)
        {
            return new Point(
                m_pPosition.X - Offset.X + a_Point.X,
                m_pPosition.Y - Offset.Y + a_Point.Y
                );
        }
        #endregion

        #region Zoom Level
        public void ToggleZoomLevel()
        {
            switch(CurrentZoom)
            {
                case Zoom.Normal:
                    SetZoomLevel(Zoom.Wide);
                    break;
                case GameCamera.Zoom.Wide:
                    SetZoomLevel(Zoom.Normal);
                    break;
            }
        }

        public void SetZoomLevel(Zoom a_NewLevel)
        {
            if (a_NewLevel == CurrentZoom)
                return;

            if (a_NewLevel == Zoom.Wide)
                TerrainWindow.Instance.Reset(160, 76, new Point(8,8)); // Hardcoded

            if (a_NewLevel == Zoom.Normal)
                TerrainWindow.Instance.Reset(80, 38, new Point(16,16)); // Hardcoded

            CurrentZoom = a_NewLevel;
        }
        #endregion    
    }
}
